前兩天我們先拆開了兩個核心概念。
Day 1 說明:
模型(LLM)負責判斷,Harness 負責讓判斷可以被執行、觀察與限制。
Day 2 則把 Agent 縮到最小,只留下:
呼叫模型
↓
檢查停止原因
↓
執行工具
↓
把結果交回模型
↓
進入下一輪
今天要繼續拆其中最容易被忽略的一步:
執行工具
很多 Framework 把它包裝成一行 API,讓人感覺模型只要產生 Tool Call,工具就會自然完成。
但 Tool Call 只是一個請求。
它不是執行結果,也不是安全邊界,更不是完整的 Tool Runtime。
假設使用者要求:
幫我讀取專案中的設定檔。
模型可能回傳:
{
"name": "read_file",
"arguments": {
"path": "config.yaml"
}
}
這段內容表示:
模型認為下一步應該呼叫
read_file,並把config.yaml當作參數。
但到這個時間點,檔案還沒有被讀取。
系統仍然不知道:
read_file 是否真的存在path 是否符合工具 Schemaconfig.yaml 是否位於允許的目錄所以 Tool Call 更接近一張工作單,而不是實際工作。
Tool Call
=
工具名稱
+
模型產生的參數
+
模型希望系統執行的意圖
真正把這張工作單轉換成環境變化的,是 Tool Runtime。
Tool Runtime 是模型與真實環境之間的執行層。
它至少要處理:
完整流程比較像:
模型產生 Tool Call
↓
Tool Registry 尋找工具
↓
Schema Validation 驗證參數
↓
Permission Gate 檢查權限
↓
Runtime 執行工具
↓
Timeout / Error Handling
↓
Output Normalization
↓
Tool Result 回到 Agent Loop
模型只參與第一步。
其他步驟都屬於 Harness。
最簡單的 Tool Runtime,通常需要一個工具註冊表。
TOOLS = {
"read_file": read_file,
"write_file": write_file,
"run_command": run_command,
}
當模型要求某個工具時,Runtime 會根據名稱尋找對應實作。
如果工具不存在,系統不應該直接 Crash。
更好的做法是產生結構化錯誤:
{
"ok": false,
"error": {
"type": "unknown_tool",
"message": "Tool is not registered."
}
}
再把錯誤交回模型。
這樣模型可以:
Tool Registry 看似簡單,但它決定了模型實際能看見哪些能力。
模型不應該能呼叫任何沒有被明確註冊的函式。
找到工具之後,不能立刻執行。
模型產生的參數可能有很多問題:
例如工具需要:
{
"path": "string",
"max_lines": "integer"
}
模型卻產生:
{
"file": "config.yaml",
"max_lines": "all"
}
如果 Runtime 直接把這組參數交給函式,可能得到難以理解的 Exception。
比較可靠的流程是:
模型參數
↓
Schema Validation
├── 通過 → 進入下一步
└── 失敗 → 回傳結構化錯誤
對模型來說,結構化錯誤比一大段 Stack Trace 更容易修正。
對系統來說,Schema Validation 也能在工具真正執行前攔截問題。
參數合法,不代表操作應該被允許。
例如模型要求讀取:
../../.ssh/id_rsa
它可能完全符合 path: string 的 Schema,但仍然不應該執行。
所以 Schema Validation 和 Permission Check 是兩個不同問題:
Schema Validation
回答:這個請求格式正確嗎?
Permission Check
回答:這個請求被允許嗎?
一個基本 Permission Gate 可能檢查:
這不是在告訴模型「不要讀取 Workspace 外的檔案」。
而是讓程式碼保證它做不到。
Prompt 是行為指引,Permission Gate 才是執行邊界。
通過工具查找、參數驗證與權限檢查後,Runtime 才能真的執行。
但工具仍可能:
因此,Production Runtime 通常需要一個共同的 Execution Wrapper。
它的價值不是讓錯誤消失。
而是把不同工具的錯誤轉換成 Agent Loop 可以處理的共同格式。
例如:
{
"ok": false,
"error": {
"type": "FileNotFoundError",
"message": "config.yaml does not exist."
}
}
模型看到這個結果後,才有機會:
如果模型呼叫一個永遠不會結束的工具,Agent Loop 也會被卡住。
例如:
所以 Runtime 不能只限制 Agent 有幾輪,也要限制每個 Tool Call 可以執行多久。
Turn Budget
限制 Agent 可以進行幾輪
Tool Timeout
限制單一工具可以執行多久
兩者解決不同問題。
當工具超過時限,Runtime 應該回傳一個可觀察的錯誤。
模型可以再決定:
但 Retry 次數仍然應由 Harness 控制,而不是讓模型無限嘗試。
工具成功執行後,也不能把任何結果直接塞回 Context。
工具可能回傳:
如果全部加入 messages[],可能立刻耗盡 Context Window。
所以 Runtime 通常需要把輸出轉換成共同格式:
{
"ok": true,
"content": "...",
"metadata": {
"truncated": false,
"duration_ms": 42
}
}
當內容太大時,可以:
Tool Result 應該提供足夠的 Observation,但不一定要把所有原始資料放進 Context。
只要 Agent 可以改變真實環境,就需要回答:
這些紀錄對三件事很重要:
如果沒有 Trace,Agent 失敗時往往只剩下最終輸出,很難知道問題發生在哪一層。
| 層級 | 主要責任 |
|---|---|
| Model | 選擇工具並產生參數 |
| Tool Schema | 描述工具可接受的輸入 |
| Tool Registry | 將工具名稱對應到實作 |
| Validator | 檢查參數是否合法 |
| Permission Gate | 判斷操作是否允許 |
| Runtime | 執行工具並處理逾時 |
| Normalizer | 將輸出轉成模型可讀格式 |
| Agent Loop | 把結果交回模型並進入下一輪 |
模型可以提出:
我想讀取這個檔案。
但它不應該同時決定:
這些是系統責任。
模型輸出是非確定性的。
即使 Tool Schema 寫得很好,Runtime 仍應該驗證。
「不要做危險操作」不是安全邊界。
真正的限制必須存在於模型外面。
Stack Trace 可能太長、包含敏感路徑,也不一定能幫助模型修正。
應該先正規化成簡短、結構化的錯誤。
大量輸出會增加 Token 成本,也可能讓真正重要的資訊被淹沒。
工具錯誤可以回到模型,但 Retry Budget 應由 Harness 限制。
不同任務、使用者與環境可能需要不同的 Tool Scope。
模型只應該看到目前真的可以使用的工具。
設計工具時,可以逐層問:
這六個問題不要混在一起。
它們分別對應不同的 Failure Mode。
Day 2 的 Agent Loop 已經可以:
但它把工具執行當成一個黑盒。
今天加入:
Tool Registry
Schema Validation
Permission Gate
Timeout
Error Normalization
Output Normalization
Audit Trace
因此,系統不再只是「模型要求什麼就執行什麼」。
而是先把 Tool Call 經過一條可控制、可觀察、可驗證的執行管線。
Tool Calling 和 Tool Runtime 是兩件不同的事。
Tool Calling 是模型提出的結構化意圖:
我想使用哪個工具,並帶入哪些參數。
Tool Runtime 則負責:
可以把它濃縮成一句話:
模型可以選擇工具,但不能自己定義工具的執行邊界。
下一篇會繼續拆解 Permission、Approval 與 Sandbox:
Agent 可以使用工具,不代表它應該被允許執行所有操作。
完整程式碼與系列內容:https://github.com/hardness1020/awesome-agent-architecture